package com.ideaaedi.springcloud.jd.commonspring.oauth2;

import com.alibaba.fastjson2.JSON;
import com.ideaaedi.springcloud.jd.commonds.constant.BaseConstant;
import com.ideaaedi.springcloud.jd.commonds.entity.BaseCodeMsgEnum;
import com.ideaaedi.springcloud.jd.commonds.entity.Result;
import com.ideaaedi.springcloud.jd.commonds.entity.user.bo.ApiInfoBO;
import com.ideaaedi.springcloud.jd.commonds.entity.user.bo.DataScopeInfoBO;
import com.ideaaedi.springcloud.jd.commonds.entity.user.bo.JwtTokenAdditionalInfoBO;
import com.ideaaedi.springcloud.jd.commonds.entity.user.vo.resp.SysApiResourceListRespVO;
import com.ideaaedi.springcloud.jd.commonds.entity.user.vo.resp.SysDataScopeDetailRespVO;
import com.ideaaedi.springcloud.jd.commonds.enums.CachePrefixEnum;
import com.ideaaedi.springcloud.jd.commonds.feign.CrossMicroServiceTransDataDTO;
import com.ideaaedi.springcloud.jd.commonspring.support.AuthService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 通用的鉴权器
 *
 * @author <font size = "20" color = "#3CAA3C"><a href="https://gitee.com/JustryDeng">JustryDeng</a></font> <img
 * src="https://gitee.com/JustryDeng/shared-files/raw/master/JustryDeng/avatar.jpg" />
 * @since 2021.0.1.A
 */
@Slf4j
@Component("commonAccessDecisor")
public class CommonAccessDecisor {
    
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    
    @Resource
    private AuthService authService;
    
    /**
     * 是否有访问的权限
     *
     * @param request 当前http请求
     * @param authentication 认证信息
     *
     * @return 鉴权是否通过
     */
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        String ifCrossMicroServiceRequestStr = request.getHeader(BaseConstant.IF_CROSS_MICRO_SERVICE_REQUEST);
        log.info("ifCrossMicroServiceRequestStr -> {}", ifCrossMicroServiceRequestStr);
        try {
            boolean ifCrossMicroServiceRequest = BooleanUtils.isTrue(Boolean.parseBoolean(ifCrossMicroServiceRequestStr));
            if (ifCrossMicroServiceRequest) {
                /*
                 * 处理来自其它微服务的请求
                 * <br />
                 * 采取内部信任机制，用户在请求其它微服务的接口时，作了认证鉴权，那么内部调用其它接口时，不再作认证鉴权
                 */
                return handleCrossMicroServiceRequest(request);
            }
            // 处理正常请求（作认证鉴权）
            return handleNormalRequest(request, authentication);
        } catch (Exception e) {
            Result<String> failureResult = Result.failure("The authentication judgment occur exception. " + e.getMessage(),
                    BaseCodeMsgEnum.AUTH_OCCUR_EXCEPTION);
            request.setAttribute(BaseConstant.OCCUR_JDX_EXCEPTION, JSON.toJSONString(failureResult));
            throw e;
        }
    }
    
    /**
     * 处理来自其它微服务的请求
     *
     * @param request 请求
     *
     * @return 是否有权限访问
     *
     * @since 2021.0.1.C
     */
    protected boolean handleCrossMicroServiceRequest(HttpServletRequest request) {
        String transData = null;
        CrossMicroServiceTransDataDTO crossMicroServiceTransData = null;
        try {
            transData = request.getHeader(BaseConstant.CROSS_MICRO_SERVICE_TRANS_DATA);
            transData = URLDecoder.decode(transData, StandardCharsets.UTF_8.name());
            crossMicroServiceTransData = JSON.parseObject(transData, CrossMicroServiceTransDataDTO.class);
            log.info("cross micro request. transData -> {}", crossMicroServiceTransData);
            addUserIdAndTenant(request, crossMicroServiceTransData.getUserId(), crossMicroServiceTransData.getTenant());
            addDataScope(request, crossMicroServiceTransData.getReadDataScopePaths(),
                    crossMicroServiceTransData.getUpdateDataScopePaths());
            return true;
        } catch (Exception e) {
            log.error("handleCrossMicroServiceRequest occur exception. transData -> {}, crossMicroServiceTransData ->"
                            + " {}",
                    transData, crossMicroServiceTransData, e);
            return false;
        }
    }
    
    /**
     * 处理正常请求
     *
     * @param request 请求
     * @param authentication 认证结果
     *
     * @return 是否有权限访问
     */
    protected boolean handleNormalRequest(HttpServletRequest request, Authentication authentication) {
        String method = request.getMethod();
        String currRequestUri = request.getRequestURI();
        String username = null;
        if (authentication != null) {
            Object principalObj = authentication.getPrincipal();
            if ((principalObj instanceof UserDetails)) {
                UserDetails principal = (UserDetails) principalObj;
                username = principal.getUsername();
            }
        }
        log.info("Auth for method -> {}, currRequestUri -> {}, username -> {}", method, currRequestUri, username);
        Collection<? extends GrantedAuthority> authorities = authentication == null ? null :
                authentication.getAuthorities();
        if (CollectionUtils.isEmpty(authorities)) {
            return false;
        }
        JwtTokenAdditionalInfoBO jwtTokenAdditionalInfo = extractAdditionalInfo(request);
        if (jwtTokenAdditionalInfo != null) {
            Long userId = jwtTokenAdditionalInfo.getUserId();
            // 判断access_api，并初始化access_tenant
            LinkedHashSet<ApiInfoBO> apiInfos = authService.queryUserApi(CachePrefixEnum.AC_USER_API, userId);
            if (apiInfos != null) {
                for (ApiInfoBO apiInfo : apiInfos) {
                    String allowMethod = apiInfo.getRequestMethod();
                    if (StringUtils.isNotBlank(allowMethod)) {
                        if (!antPathMatcher.match(allowMethod.toLowerCase(Locale.ENGLISH),
                                method.toLowerCase(Locale.ENGLISH))) {
                            continue;
                        }
                    }
                    String allowPath = apiInfo.getPath();
                    if (antPathMatcher.match(allowPath, currRequestUri)) {
                        Long currUserId = (Long) request.getAttribute(BaseConstant.CURR_USER_ID);
                        if (currUserId == null) {
                            // 初始化access_tenant
                            String tenant = authService.queryUserTenant(CachePrefixEnum.AC_USER_TENANT, userId);
                            addUserIdAndTenant(request, userId, tenant);
                            
                            // 初始化access_data-scope
                            SysDataScopeDetailRespVO dataScopeDetail =
                                    authService.queryUserDataScope(CachePrefixEnum.AC_USER_DATA_SCOPE, userId);
                            List<DataScopeInfoBO> readDataScopeList = dataScopeDetail.getReadDataScopeList();
                            List<DataScopeInfoBO> updateDataScopeList = dataScopeDetail.getUpdateDataScopeList();
                            request.setAttribute(
                                    BaseConstant.CURR_USER_READ_DATA_SCOPE_PATHS,
                                    JSON.toJSONString(filterAndDistinctDeptPath(readDataScopeList, currRequestUri))
                            );
                            request.setAttribute(
                                    BaseConstant.CURR_USER_UPDATE_DATA_SCOPE_PATHS,
                                    JSON.toJSONString(filterAndDistinctDeptPath(updateDataScopeList, currRequestUri))
                            );
                        }
                        return true;
                    }
                }
            }
        }
        return false;
    }
    
    /**
     * 过滤掉不适用于当前uri的数据范围，并将剩下的数据范围进行重叠部分去重并返回deptPath集合
     *
     * @param list 原始数据范围数据
     * @param currRequestUri 当前请求的uri
     *
     * @return deptPath集合
     */
    @NonNull
    protected LinkedHashSet<String> filterAndDistinctDeptPath(@Nullable List<DataScopeInfoBO> list,
                                                              @NonNull String currRequestUri) {
        if (CollectionUtils.isEmpty(list)) {
            return new LinkedHashSet<>();
        }
        // step1. 过滤掉不适用于当前uri的数据范围
        list = list.stream().filter(x -> {
            List<SysApiResourceListRespVO> apiList = x.getApiList();
            // 为空，则表示当前数据范围适用于所有uri，直接返回true
            if (CollectionUtils.isEmpty(apiList)) {
                return true;
            }
            for (SysApiResourceListRespVO sysApiResourceList : apiList) {
                String path = sysApiResourceList.getPath();
                if (StringUtils.isBlank(path)) {
                    continue;
                }
                if (antPathMatcher.match(path, currRequestUri)) {
                    return true;
                }
            }
            return false;
        }).collect(Collectors.toList());
        if (CollectionUtils.isEmpty(list)) {
            return new LinkedHashSet<>();
        }
        
        // step2. 将剩下的数据范围进行重叠部分去重并返回deptPath集合
        List<String> deptPathList = list.stream().flatMap(x -> {
            Set<String> deptPathSet = new HashSet<>();
            String deptPath = x.getDeptPath();
            deptPathSet.add(deptPath);
            Set<String> deptHisPathSet = x.getDeptHisPathSet();
            if (!CollectionUtils.isEmpty(deptHisPathSet)) {
                deptPathSet.addAll(deptHisPathSet);
            }
            return deptPathSet.stream();
        }).sorted(Comparator.comparing(String::length)).collect(Collectors.toList());
        Set<String> superfluousDeptPathSet = new HashSet<>();
        for (int i = 0, size = deptPathList.size(); i < size; i++) {
            String xDeptPath = deptPathList.get(i);
            if (i >= size - 1) {
                break;
            }
            for (int j = i + 1; j < size; j++) {
                String yDeptPath = deptPathList.get(j);
                if (yDeptPath.startsWith(xDeptPath)) {
                    superfluousDeptPathSet.add(yDeptPath);
                }
            }
        }
        LinkedHashSet<String> deptPathSet = new LinkedHashSet<>(deptPathList);
        deptPathSet.removeAll(superfluousDeptPathSet);
        return deptPathSet;
    }
    
    /**
     * 添加userId、tenant至请求attribute
     *
     * @param request 当前请求
     * @param userId 用户id
     * @param tenant 租户
     */
    protected void addUserIdAndTenant(@NonNull HttpServletRequest request, Long userId, String tenant) {
        if (userId != null) {
            // 将用户id放进request
            request.setAttribute(BaseConstant.CURR_USER_ID, userId);
            // 将用户id放进traceId
            String traceId = MDC.get(BaseConstant.TRACE_ID);
            //noinspection AlibabaUndefineMagicConstant
            if (traceId != null && traceId.contains("%s")) {
                String newTraceId = String.format(traceId, userId);
                MDC.put(BaseConstant.TRACE_ID, newTraceId);
                log.info("Upgrade traceId from  [{}] to [{}]", traceId, newTraceId);
            }
        }
        if (StringUtils.isNotBlank(tenant)) {
            // 将用户所属租户放进request
            request.setAttribute(BaseConstant.CURR_USER_TENANT, tenant);
        }
    }
    
    /**
     * 添加userId、tenant至请求attribute
     *
     * @param request 当前请求
     * @param readDataScopePaths 可读数据范围的deptId对应的deptPath
     * @param updateDataScopePaths 可写(修改/删除)数据范围的deptId对应的deptPath
     */
    protected void addDataScope(@NonNull HttpServletRequest request,
                                @Nullable LinkedHashSet<String> readDataScopePaths,
                                @Nullable LinkedHashSet<String> updateDataScopePaths) {
        if (!CollectionUtils.isEmpty(readDataScopePaths)) {
            request.setAttribute(BaseConstant.CURR_USER_READ_DATA_SCOPE_PATHS, JSON.toJSONString(readDataScopePaths));
        }
        if (!CollectionUtils.isEmpty(updateDataScopePaths)) {
            request.setAttribute(BaseConstant.CURR_USER_UPDATE_DATA_SCOPE_PATHS,
                    JSON.toJSONString(updateDataScopePaths));
        }
    }
    
    /**
     * 从jwtToken中抽取附加信息
     *
     * @param request 当前请求
     *
     * @return 附加信息
     */
    @Nullable
    protected JwtTokenAdditionalInfoBO extractAdditionalInfo(HttpServletRequest request) {
        String userLoginInfoJson = null;
        if (request == null) {
            return null;
        }
        String jwtToken = request.getHeader(BaseConstant.JWT_TOKEN_KEY);
        if (StringUtils.isBlank(jwtToken)) {
            return null;
        }
        if (!jwtToken.startsWith(BaseConstant.JWT_TOKEN_PREFIX)) {
            return null;
        }
        jwtToken = jwtToken.substring(BaseConstant.JWT_TOKEN_PREFIX.length()).trim();
        try {
            int fromIdx = jwtToken.indexOf(".");
            int toIdx = jwtToken.lastIndexOf(".");
            String userLoginInfoBase64 = jwtToken.substring(fromIdx, toIdx + 1);
            userLoginInfoJson =
                    new String(Base64.getMimeDecoder().decode(userLoginInfoBase64.getBytes(StandardCharsets.UTF_8)));
            return JSON.parseObject(userLoginInfoJson, JwtTokenAdditionalInfoBO.class);
        } catch (Exception e) {
            log.warn("extractAdditionalInfo fail, curr userLoginInfoJson is -> {}", userLoginInfoJson);
            return null;
        }
    }
}
